/*
 * Copyright (C) 2011-2015 René Jeschke <rene_jeschke@yahoo.de>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.rjeschke.txtmark;

/**
 * Utilities.
 *
 * @author René Jeschke <rene_jeschke@yahoo.de>
 */
class Utils {
    /**
     * Random number generator value.
     */
    private static int RND = (int) System.nanoTime();

    /**
     * LCG random number generator.
     *
     * @return A pseudo random number between 0 and 1023
     */
    public final static int rnd() {
        return (RND = RND * 1664525 + 1013904223) >>> 22;
    }

    /**
     * Skips spaces in the given String.
     *
     * @param in    Input String.
     * @param start Starting position.
     * @return The new position or -1 if EOL has been reached.
     */
    public final static int skipSpaces(final String in, final int start) {
        int pos = start;
        while (pos < in.length() && (in.charAt(pos) == ' ' || in.charAt(pos) == '\n')) {
            pos++;
        }
        return pos < in.length() ? pos : -1;
    }

    /**
     * Processed the given escape sequence.
     *
     * @param out The StringBuilder to write to.
     * @param ch  The character.
     * @param pos Current parsing position.
     * @return The new position.
     */
    public final static int escape(final StringBuilder out, final char ch, final int pos) {
        switch (ch) {
            case '\\':
            case '[':
            case ']':
            case '(':
            case ')':
            case '{':
            case '}':
            case '#':
            case '"':
            case '\'':
            case '.':
            case '>':
            case '<':
            case '*':
            case '+':
            case '-':
            case '_':
            case '!':
            case '`':
            case '^':
                out.append(ch);
                return pos + 1;
            default:
                out.append('\\');
                return pos;
        }
    }

    /**
     * Reads characters until any 'end' character is encountered.
     *
     * @param out   The StringBuilder to write to.
     * @param in    The Input String.
     * @param start Starting position.
     * @param end   End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readUntil(final StringBuilder out, final String in, final int start, final char... end) {
        int pos = start;
        while (pos < in.length()) {
            final char ch = in.charAt(pos);
            if (ch == '\\' && pos + 1 < in.length()) {
                pos = escape(out, in.charAt(pos + 1), pos);
            } else {
                boolean endReached = false;
                for (int n = 0; n < end.length; n++) {
                    if (ch == end[n]) {
                        endReached = true;
                        break;
                    }
                }
                if (endReached) {
                    break;
                }
                out.append(ch);
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until the 'end' character is encountered.
     *
     * @param out   The StringBuilder to write to.
     * @param in    The Input String.
     * @param start Starting position.
     * @param end   End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readUntil(final StringBuilder out, final String in, final int start, final char end) {
        int pos = start;
        while (pos < in.length()) {
            final char ch = in.charAt(pos);
            if (ch == '\\' && pos + 1 < in.length()) {
                pos = escape(out, in.charAt(pos + 1), pos);
            } else {
                if (ch == end) {
                    break;
                }
                out.append(ch);
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads a markdown link.
     *
     * @param out   The StringBuilder to write to.
     * @param in    Input String.
     * @param start Starting position.
     * @return The new position or -1 if this is no valid markdown link.
     */
    public final static int readMdLink(final StringBuilder out, final String in, final int start) {
        int pos = start;
        int counter = 1;
        while (pos < in.length()) {
            final char ch = in.charAt(pos);
            if (ch == '\\' && pos + 1 < in.length()) {
                pos = escape(out, in.charAt(pos + 1), pos);
            } else {
                boolean endReached = false;
                switch (ch) {
                    case '(':
                        counter++;
                        break;
                    case ' ':
                        if (counter == 1) {
                            endReached = true;
                        }
                        break;
                    case ')':
                        counter--;
                        if (counter == 0) {
                            endReached = true;
                        }
                        break;
                }
                if (endReached) {
                    break;
                }
                out.append(ch);
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads a markdown link ID.
     *
     * @param out   The StringBuilder to write to.
     * @param in    Input String.
     * @param start Starting position.
     * @return The new position or -1 if this is no valid markdown link ID.
     */
    public final static int readMdLinkId(final StringBuilder out, final String in, final int start) {
        int pos = start;
        int counter = 1;
        while (pos < in.length()) {
            final char ch = in.charAt(pos);
            boolean endReached = false;
            switch (ch) {
                case '\n':
                    out.append(' ');
                    break;
                case '[':
                    counter++;
                    out.append(ch);
                    break;
                case ']':
                    counter--;
                    if (counter == 0) {
                        endReached = true;
                    } else {
                        out.append(ch);
                    }
                    break;
                default:
                    out.append(ch);
                    break;
            }
            if (endReached) {
                break;
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until any 'end' character is encountered, ignoring
     * escape sequences.
     *
     * @param out   The StringBuilder to write to.
     * @param in    The Input String.
     * @param start Starting position.
     * @param end   End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readRawUntil(final StringBuilder out, final String in, final int start, final char... end) {
        int pos = start;
        while (pos < in.length()) {
            final char ch = in.charAt(pos);
            boolean endReached = false;
            for (int n = 0; n < end.length; n++) {
                if (ch == end[n]) {
                    endReached = true;
                    break;
                }
            }
            if (endReached) {
                break;
            }
            out.append(ch);
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until the end character is encountered, taking care of
     * HTML/XML strings.
     *
     * @param out   The StringBuilder to write to.
     * @param in    The Input String.
     * @param start Starting position.
     * @param end   End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readRawUntil(final StringBuilder out, final String in, final int start, final char end) {
        int pos = start;
        while (pos < in.length()) {
            final char ch = in.charAt(pos);
            if (ch == end) {
                break;
            }
            out.append(ch);
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until any 'end' character is encountered, ignoring
     * escape sequences.
     *
     * @param out   The StringBuilder to write to.
     * @param in    The Input String.
     * @param start Starting position.
     * @param end   End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readXMLUntil(final StringBuilder out, final String in, final int start, final char... end) {
        int pos = start;
        boolean inString = false;
        char stringChar = 0;
        while (pos < in.length()) {
            final char ch = in.charAt(pos);
            if (inString) {
                if (ch == '\\') {
                    out.append(ch);
                    pos++;
                    if (pos < in.length()) {
                        out.append(ch);
                        pos++;
                    }
                    continue;
                }
                if (ch == stringChar) {
                    inString = false;
                    out.append(ch);
                    pos++;
                    continue;
                }
            }
            switch (ch) {
                case '"':
                case '\'':
                    inString = true;
                    stringChar = ch;
                    break;
            }
            if (!inString) {
                boolean endReached = false;
                for (int n = 0; n < end.length; n++) {
                    if (ch == end[n]) {
                        endReached = true;
                        break;
                    }
                }
                if (endReached) {
                    break;
                }
            }
            out.append(ch);
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Appends the given string encoding special HTML characters.
     *
     * @param out   The StringBuilder to write to.
     * @param in    Input String.
     * @param start Input String starting position.
     * @param end   Input String end position.
     */
    public final static void appendCode(final StringBuilder out, final String in, final int start, final int end) {
        for (int i = start; i < end; i++) {
            final char c;
            switch (c = in.charAt(i)) {
                case '&':
                    out.append("&amp;");
                    break;
                case '<':
                    out.append("&lt;");
                    break;
                case '>':
                    out.append("&gt;");
                    break;
                default:
                    out.append(c);
                    break;
            }
        }
    }

    /**
     * Appends the given string encoding special HTML characters (used in HTML
     * attribute values).
     *
     * @param out   The StringBuilder to write to.
     * @param in    Input String.
     * @param start Input String starting position.
     * @param end   Input String end position.
     */
    public final static void appendValue(final StringBuilder out, final String in, final int start, final int end) {
        for (int i = start; i < end; i++) {
            final char c;
            switch (c = in.charAt(i)) {
                case '&':
                    out.append("&amp;");
                    break;
                case '<':
                    out.append("&lt;");
                    break;
                case '>':
                    out.append("&gt;");
                    break;
                case '"':
                    out.append("&quot;");
                    break;
                case '\'':
                    out.append("&apos;");
                    break;
                default:
                    out.append(c);
                    break;
            }
        }
    }

    /**
     * Append the given char as a decimal HTML entity.
     *
     * @param out   The StringBuilder to write to.
     * @param value The character.
     */
    public final static void appendDecEntity(final StringBuilder out, final char value) {
        out.append("&#");
        out.append((int) value);
        out.append(';');
    }

    /**
     * Append the given char as a hexadecimal HTML entity.
     *
     * @param out   The StringBuilder to write to.
     * @param value The character.
     */
    public final static void appendHexEntity(final StringBuilder out, final char value) {
        out.append("&#x");
        out.append(Integer.toHexString(value));
        out.append(';');
    }

    /**
     * Appends the given mailto link using obfuscation.
     *
     * @param out   The StringBuilder to write to.
     * @param in    Input String.
     * @param start Input String starting position.
     * @param end   Input String end position.
     */
    public final static void appendMailto(final StringBuilder out, final String in, final int start, final int end) {
        for (int i = start; i < end; i++) {
            final char c;
            final int r = rnd();
            switch (c = in.charAt(i)) {
                case '&':
                case '<':
                case '>':
                case '"':
                case '\'':
                case '@':
                    if (r < 512) {
                        appendDecEntity(out, c);
                    } else {
                        appendHexEntity(out, c);
                    }
                    break;
                default:
                    if (r < 32) {
                        out.append(c);
                    } else if (r < 520) {
                        appendDecEntity(out, c);
                    } else {
                        appendHexEntity(out, c);
                    }
                    break;
            }
        }
    }

    /**
     * Extracts the tag from an XML element.
     *
     * @param out The StringBuilder to write to.
     * @param in  Input StringBuilder.
     */
    public final static void getXMLTag(final StringBuilder out, final StringBuilder in) {
        int pos = 1;
        if (in.charAt(1) == '/') {
            pos++;
        }
        while (Character.isLetterOrDigit(in.charAt(pos))) {
            out.append(in.charAt(pos++));
        }
    }

    /**
     * Extracts the tag from an XML element.
     *
     * @param out The StringBuilder to write to.
     * @param in  Input String.
     */
    public final static void getXMLTag(final StringBuilder out, final String in) {
        int pos = 1;
        if (in.charAt(1) == '/') {
            pos++;
        }
        while (Character.isLetterOrDigit(in.charAt(pos))) {
            out.append(in.charAt(pos++));
        }
    }

    /**
     * Reads an XML element.
     *
     * @param out      The StringBuilder to write to.
     * @param in       Input String.
     * @param start    Starting position.
     * @param safeMode Whether to escape unsafe HTML tags or not
     * @return The new position or -1 if this is no valid XML element.
     */
    public final static int readXML(final StringBuilder out, final String in, final int start, final boolean safeMode) {
        int pos;
        final boolean isCloseTag;
        try {
            if (in.charAt(start + 1) == '/') {
                isCloseTag = true;
                pos = start + 2;
            } else if (in.charAt(start + 1) == '!') {
                out.append("<!");
                return start + 1;
            } else {
                isCloseTag = false;
                pos = start + 1;
            }
            if (safeMode) {
                final StringBuilder temp = new StringBuilder();
                pos = readXMLUntil(temp, in, pos, ' ', '/', '>');
                if (pos == -1) {
                    return -1;
                }
                final String tag = temp.toString().trim().toLowerCase();
                if (HTML.isUnsafeHtmlElement(tag)) {
                    out.append("&lt;");
                } else {
                    out.append("<");
                }
                if (isCloseTag) {
                    out.append('/');
                }
                out.append(temp);
            } else {
                out.append('<');
                if (isCloseTag) {
                    out.append('/');
                }
                pos = readXMLUntil(out, in, pos, ' ', '/', '>');
            }
            if (pos == -1) {
                return -1;
            }
            pos = readXMLUntil(out, in, pos, '/', '>');
            if (in.charAt(pos) == '/') {
                out.append(" /");
                pos = readXMLUntil(out, in, pos + 1, '>');
                if (pos == -1) {
                    return -1;
                }
            }
            if (in.charAt(pos) == '>') {
                out.append('>');
                return pos;
            }
        } catch (final StringIndexOutOfBoundsException e) {
            return -1;
        }
        return -1;
    }

    /**
     * Appends the given string to the given StringBuilder, replacing '&amp;',
     * '&lt;' and '&gt;' by their respective HTML entities.
     *
     * @param out    The StringBuilder to append to.
     * @param value  The string to append.
     * @param offset The character offset into value from where to start
     */
    public final static void codeEncode(final StringBuilder out, final String value, final int offset) {
        for (int i = offset; i < value.length(); i++) {
            final char c = value.charAt(i);
            switch (c) {
                case '&':
                    out.append("&amp;");
                    break;
                case '<':
                    out.append("&lt;");
                    break;
                case '>':
                    out.append("&gt;");
                    break;
                default:
                    out.append(c);
            }
        }
    }

    /**
     * Removes trailing <code>`</code> or <code>~</code> and trims spaces.
     *
     * @param fenceLine Fenced code block starting line
     * @return Rest of the line after trimming and backtick or tilde removal
     * @since 0.7
     */
    public final static String getMetaFromFence(final String fenceLine) {
        for (int i = 0; i < fenceLine.length(); i++) {
            final char c = fenceLine.charAt(i);
            if (!Character.isWhitespace(c) && c != '`' && c != '~') {
                return fenceLine.substring(i).trim();
            }
        }
        return "";
    }
}
